Skip to main content

systemd service pattern

Each API runs as its own systemd unit. Units live in /etc/systemd/system/<api-name>.service.

Standard unit file

See templates/api.service.template for the canonical copy. Key design choices:

SettingValueWhy
User= / Group=apisvcUnprivileged dedicated account
WorkingDirectory=/opt/apis/<api-name>Relative imports and app paths work
EnvironmentFile=/etc/<api-name>/<api-name>.envSecrets outside git
--host 127.0.0.1localhost onlyNginx is the only public entry
Restart=alwaysauto-restartSurvives crashes and reboots
RestartSec=5delay before restartAvoid tight crash loops

FastAPI + Uvicorn (default)

ExecStart=/opt/venvs/<api-name>/bin/uvicorn app.main:app \
--host 127.0.0.1 \
--port 8001

Adjust app.main:app to match the project's ASGI application object.

Flask + Gunicorn (alternative)

ExecStart=/opt/venvs/<api-name>/bin/gunicorn \
--bind 127.0.0.1:8001 \
--workers 2 \
app:app

Use Gunicorn when the project is WSGI (Flask, Django without ASGI). Worker count: commonly 2 * CPU + 1 for CPU-bound work; start with 2 on a laptop server.

Environment file format

/etc/<api-name>/<api-name>.env uses systemd EnvironmentFile syntax:

  • One KEY=value per line
  • No export prefix
  • Quotes optional; use quotes if values contain spaces
  • Lines starting with # are comments

Example:

ENV=production
LOG_LEVEL=info
AZURE_STORAGE_CONNECTION_STRING=DefaultEndpointsProtocol=https;...

After editing:

sudo systemctl restart <api-name>

systemd reads the file at start time; changes require a restart (not reload) unless using systemctl daemon-reload for unit file changes only.

Logging

stdout/stderr go to the journal by default:

# Follow live logs
journalctl -u <api-name> -f

# Last 100 lines
journalctl -u <api-name> -n 100 --no-pager

# Since last boot
journalctl -u <api-name> -b

Optional file logging: add StandardOutput=append:/var/log/<api-name>/app.log and ensure /var/log/<api-name>/ exists with apisvc ownership. Journal logging is usually sufficient.

Common operations

# Apply unit file changes
sudo systemctl daemon-reload
sudo systemctl restart <api-name>

# Enable on boot
sudo systemctl enable <api-name>

# Status
sudo systemctl status <api-name>

# Stop temporarily
sudo systemctl stop <api-name>

Security hardening (optional)

For tighter isolation, add to the [Service] section:

NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/var/log/<api-name>

Test thoroughly after enabling ProtectSystem=strict — some apps need write access to unexpected paths.

Dependency ordering

If an API must start after network is up (default):

[Unit]
After=network-online.target
Wants=network-online.target

If multiple APIs depend on each other (uncommon), use After=<other-api>.service — prefer loose coupling via HTTP instead.